%%%-----------------------------------------------------------------------------
%%% @doc
%%% recompiler and reloader.
%%%
%%% @end
%%%-----------------------------------------------------------------------------
-module(reloader).
-author("Matthew Dempsky <matthew@mochimedia.com>").

-include_lib("kernel/include/file.hrl").

-behaviour(gen_server).
-export([start/0, start_link/0]).
-export([stop/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([all_changed/0]).
-export([is_changed/1]).
-export([reload_modules/1, reload_app/1, reload/1]).
-record(state, {last, tref}).

-define(TIMER, 2000).

%% External API

%% @spec start() -> ServerRet
%% @doc Start the reloader.
start() ->
    gen_server:start({local, ?MODULE}, ?MODULE, [], []).

%% @spec start_link() -> ServerRet
%% @doc Start the reloader.
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

%% @spec stop() -> ok
%% @doc Stop the reloader.
stop() ->
    gen_server:call(?MODULE, stop).

%% gen_server callbacks

%% @spec init([]) -> {ok, State}
%% @doc gen_server init, opens the server in an initial state.
init([]) ->
    % {ok, TRef} = timer:send_interval(timer:seconds(1), doit),
    % {ok, #state{last = stamp(), tref = TRef}}.
    erlang:send_after(?TIMER, self(), doit),
    {ok, #state{last = stamp()}}.
 
%% @spec handle_call(Args, From, State) -> tuple()
%% @doc gen_server callback.
handle_call(stop, _From, State) ->
    {stop, shutdown, stopped, State};
handle_call(_Req, _From, State) ->
    {reply, {error, badrequest}, State}.

%% @spec handle_cast(Cast, State) -> tuple()
%% @doc gen_server callback.
handle_cast(_Req, State) ->
    {noreply, State}.

%% @spec handle_info(Info, State) -> tuple()
%% @doc gen_server callback.
handle_info(doit, State) ->
    Now = stamp(),
	reload_config:reload(),
    doit(State#state.last, Now),
    erlang:send_after(?TIMER, self(), doit),
    {noreply, State#state{last = Now}};
handle_info(_Info, State) ->
    {noreply, State}.

%% @spec terminate(Reason, State) -> ok
%% @doc gen_server termination callback.
terminate(_Reason, _State) ->
    % {ok, cancel} = timer:cancel(State#state.tref),
    ok.


%% @spec code_change(_OldVsn, State, _Extra) -> State
%% @doc gen_server code_change callback (trivial).
code_change(_Vsn, State, _Extra) ->
    {ok, State}.

%% @spec reload_modules([atom()]) -> [{module, atom()} | {error, term()}]
%% @doc code:purge/1 and code:load_file/1 the given list of modules in order,
%%      return the results of code:load_file/1.
reload_modules(Modules) ->
    % [begin code:purge(M), code:load_file(M) end || M <- Modules].
    io:format("length~p~n", [length(Modules)]),
    [reload(M) || M <- Modules].
    
%% @spec all_changed() -> [atom()]
%% @doc Return a list of beam modules that have changed.
all_changed() ->
    [M || {M, Fn} <- code:all_loaded(), is_list(Fn), is_changed(M)].

%% @spec is_changed(atom()) -> boolean()
%% @doc true if the loaded module is a beam with a vsn attribute
%%      and does not match the on-disk beam file, returns false otherwise.
is_changed(M) ->
    try
        module_vsn(M:module_info()) =/= module_vsn(code:get_object_code(M))
    catch _:_ ->
            false
    end.

%% Internal API

module_vsn({M, Beam, _Fn}) ->
    {ok, {M, Vsn}} = beam_lib:version(Beam),
    Vsn;
module_vsn(L) when is_list(L) ->
    {_, Attrs} = lists:keyfind(attributes, 1, L),
    {_, Vsn} = lists:keyfind(vsn, 1, Attrs),
    Vsn.

to_atom(Atom) when is_list(Atom)  -> 
    try
        erlang:list_to_existing_atom(Atom)
    catch
        _:_ -> 
            erlang:list_to_atom(Atom)
    end;
to_atom(Atom) -> 
    Atom.

% "/usr/dir/filename.ext"  ".ext"
filename_to_modulename(Filename, Ext) ->
    Basename = filename:basename(Filename),
    case string:right(Basename, string:len(Ext)) == Ext of 
        true ->
            to_atom(string:left(Basename, string:len(Basename)-string:len(Ext)));
        _ ->
            unknowned
    end.

get_includes() ->
    lists:foldl(
        fun(X, {R1, R2, R3}) -> 
            {Includedir, Srcdir} = case string:right(X, 5) == "/ebin" of 
                        true -> 
                            {[{i, filename:dirname(X) ++ "/include"}], filename:dirname(X) ++ "/src"};
                        _ ->
                            {[], X} 
                    end,

            {Includedir ++ R1, 
            [Srcdir|R2], 
            [X|R3]}
        end, {[], [], []}, code:get_path()).

doit(From, To) ->
    {_Includes, _Srcdirs, EBindirs} = get_includes(),
    % io:format("doit(~p)~n", [{Includes, Srcdirs, EBindirs}]),

    lists:map(
        fun(X) ->
             filelib:fold_files(X, ".*.beam", false, fun(F, _AccIn) -> try_reload(filename_to_modulename(F, ".beam"), F, From, To, []) end, [])
        end, EBindirs).

try_reload(Module, Filename, From, To, _ReloadedBeams) ->
    % io:format("try_reload(~p)~n", [{Filename, Module}]),
    case file:read_file_info(Filename) of
         {ok, #file_info{mtime = Mtime}} when Mtime >= From, Mtime < To ->
             reload(Module);
         {ok, _} ->
             unmodified;
         {error, enoent} ->
             %% The Erlang compiler deletes existing .beam files if
             %% recompiling fails.  Maybe it's worth spitting out a
             %% warning here, but I'd want to limit it to just once.
             gone;
         {error, enotdir} ->
            gone;
         {error, Reason} ->
             io:format("Error reading ~s's file info: ~p~n",
                       [Filename, Reason]),
             error
     end.

reload_app(Appname) ->
    {_Includes, _Srcdirs, EBindirs} = get_includes(),
    EBindir = 
        lists:foldl(
            fun(X, Tmpdir) ->
                case string:str(X, Appname++"/ebin") > 0 of 
                    true ->
                        X;
                    _ ->
                        Tmpdir
                end
            end, "", EBindirs),

    filelib:fold_files(EBindir, ".*.beam", false, fun(F, _AccIn) -> reload(filename_to_modulename(F, ".beam")) end, []).

reload(Module) ->
    io:format("Reloading ~p ...~n", [Module]),
    code:purge(Module),
    case code:load_file(Module) of
        {module, Module} ->
            io:format(" ok.~n"),
            case erlang:function_exported(Module, test, 0) of
                true ->
                    io:format(" - Calling ~p:test() ...~n", [Module]),
                    case catch Module:test() of
                        ok ->
                            io:format(" ok.~n"),
                            reload;
                        Reason ->
                            io:format(" fail: ~p.~n", [Reason]),
                            reload_but_test_failed
                    end;
                false ->
                    reload
            end;
        {error, Reason} ->
            io:format(" fail: ~p.~n", [Reason]),
            error
    end.


stamp() ->
    erlang:localtime().

%%
%% Tests
%%
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
